package com.wwu.system.common.security;

import com.wwu.system.entity.SysUser;
import com.wwu.system.service.ISysUserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import javax.annotation.Resource;
import javax.sql.DataSource;

/**
 * SpringSecurity安全框架配置类
 * @author 一蓑烟雨
 * @date 2021-10-27 17:55
 * @version 1.0.0
 **/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig  extends WebSecurityConfigurerAdapter {
    /** 登陆成功处理器 */
    @Resource
    private SecurityLoginSuccessHandler securityLoginSuccessHandler;
    /** 登陆失败处理器 */
    @Resource
    private SecurityLoginFailedHandler securityLoginFailedHandler;
    /** 退出成功处理器 */
    @Resource
    private SecurityLogoutSuccessHandler securityLogoutSuccessHandler;
    /** 账号并发登陆处理器 */
    @Resource
    private ConcurrentLoginHandler concurrentLoginHandler;
    /** 登陆验证码校验过滤器 */
    @Resource
    private SecurityCaptchaVerifyFilter securityCaptchaVerifyFilter;
    /** 数据源（记住我存储到数据库中） */
    @Resource
    private DataSource dataSource;
    /** @Lazy 为懒加载，避免循环依赖 */
    @Resource
    @Lazy
    private ISysUserService sysUserService;

    /**
     *  放行静态资源
     * @author 一蓑烟雨
     * @date 2021/10/29 0029 下午 19:15
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/css/**",
                "/images/**",
                "/js/**",
                "/error/**",
                "/layuimini/**",
                "/swagger-resources/**",
                "/v2/api-docs/**",
                "/v2/api-docs-ext/**",
                "/doc.html",
                "/webjars/**",
                "/ws/**"
        );
    }

    /**
     * 配置权限认证策略
     * @author 一蓑烟雨
     * @date 2021/10/27 0027 下午 19:39
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //1.禁用csrf
        http.csrf()
                .disable()
                //添加验证码校验过滤器
                .addFilterBefore(securityCaptchaVerifyFilter, UsernamePasswordAuthenticationFilter.class)
                //允许iframe页面嵌套
                .headers().frameOptions().disable();

        //2.设置登陆页、登陆和退出
        http.formLogin()
                //前端表单用户名的name属性值(默认为username)，如不是需要在此修改
                .usernameParameter("username")
                //前端表单密码的name属性值(默认为password)，如不是需要在此修改
                .passwordParameter("password")
                //设置登陆页
                .loginPage("/")
                //使用SpringSecurity框架提供的登陆表单(/login与前端登陆提交路径一致），登陆逻辑在下方的UserDetailsService中
                .loginProcessingUrl("/login")
                //登陆成功处理器
                .successHandler(securityLoginSuccessHandler)
                //登陆失败处理器
                .failureHandler(securityLoginFailedHandler)
                //退出登陆配置
                .and()
                .logout()
                //使用SpringSecurity框架提供的退出登陆(/logout与前端退出按钮提交路径一致）
                .logoutUrl("/logout")
                //删除SpringSecurity框架登陆后产生的cookies信息
                .deleteCookies("JSESSIONID")
                //清除Session信息
                .invalidateHttpSession(true)
                //退出成功处理器
                .logoutSuccessHandler(securityLogoutSuccessHandler)
                //记住密码配置(可配置到将token存储到客户端浏览器、服务器内存或数据库中)
                .and()
                .rememberMe()
                //ajax提交前端表单则为data中的参数名
                .rememberMeParameter("rememberMe")
                //保存到浏览器端的Cookie有效期(默认为2周)
                .tokenValiditySeconds(7*24*60*60)
                //保存到浏览器端的Cookie名称(默认为remember-me)，可进行修改
                .rememberMeCookieName("remember-me")
                //自定义登陆逻辑
                .userDetailsService(userDetailsService())
                //自定义token存储方式为数据库持久化
                .tokenRepository(persistentTokenRepository());

        //3.访问权限设置
        http.authorizeRequests()
                //放行获取验证码、登陆和登陆请求
                .antMatchers("/","/login","/captcha").permitAll()
                //所有请求都需要认证(不需要认证的在WebSecurity参数的configure中定义)
                .anyRequest().authenticated();

        //4.限制账号只能同时登陆20个
        http.sessionManagement()
                //配置登陆并发数量
                .maximumSessions(20)
                //是否保留旧用户，若保留则在新的设备无法登陆
                .maxSessionsPreventsLogin(false)
                //保留旧用户，达到最大并发数时的处理器时旧用户处理器
                .expiredSessionStrategy(concurrentLoginHandler)
                //设置会话注册类(统计在线用户)
                .sessionRegistry(sessionRegistry());

    }

    /**
     * SpringSecurity提供的登陆操作逻辑，返回UserDetails接口的实现类SysUser实体
     * 重写userDetailsService接口中的loadUserByUsername方法
     * @author 一蓑烟雨
     * @date 2021/10/27 0027 下午 19:12
     */
    @Bean
    @Override
    public UserDetailsService userDetailsService(){
        return  username -> {
            SysUser sysUser = sysUserService.getUserByUserName(username);
            if (null != sysUser) {
                return sysUser;
            }
            throw new UsernameNotFoundException("用户名或密码不正确");
        };
    }

    /**
     * 配置自定义身份认证管理器
     * @author 一蓑烟雨
     * @date 2021/10/27 0027 下午 19:19
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
    }

    /**
     * 身份认证逻辑
     * @return org.springframework.security.authentication.dao.DaoAuthenticationProvider
     * @author 一蓑烟雨
     * @date 2023/6/16 18:11
     */
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        //设置hideUserNotFoundExceptions为false，否则登陆失败无法捕获
        provider.setHideUserNotFoundExceptions(false);
        //SpringSecurity框架使用上述登陆操作逻辑方法
        provider.setUserDetailsService(userDetailsService());
        //SpringSecurity框架密码加密规则
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    /**
     * 密码加密器
     * @author 一蓑烟雨
     * @date 2021/10/27 0027 下午 19:16
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    /**
     * 记住我，将登陆信息持久化到数据库persistent_logins表中
     * 通过tokenRepository.setCreateTableOnStartup(true)设置启动时创建persistent_logins表;
     * @return org.springframework.security.web.authentication.rememberMe.PersistentTokenRepository
     * @author 一蓑烟雨
     * @date 2023/6/4 21:24
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        return tokenRepository;
    }


    /**
     * 注入Session注册器,使用该类清理过期或已注销的会话
     * @return org.springframework.security.web.session.HttpSessionEventPublisher
     * @author 一蓑烟雨
     * @date 2023/7/26 13:31
     */
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher(){
        return new HttpSessionEventPublisher();
    }

    /**
     * 注入Session注册器,使用该类获取在线的全部用户
     * @return org.springframework.security.core.session.SessionRegistry
     * @author 一蓑烟雨
     * @date 2023/7/26 8:58
     */
    @Bean
    public SessionRegistry sessionRegistry(){
        return new SessionRegistryImpl();
    }
}